#pragma once

#include <cstddef>
#include <stdexcept>
#include <type_traits>
#include <utility>

using namespace std;

namespace BR {

template<class Tp>
class Point2D
{
private:
	static string const POINT2D_OUT_OF_RANGE_MESSAGE("Point2D::operator[]");

public:
	Tp x, y;

	typedef Tp value_type;
	enum idx_type{ X=0, Y=1 };

	Point2D(void) : x(), y() { };

	constexpr Point2D(Point2D const &) = default;

	constexpr Point2D(Point2D &&) = default;

	Point2D(Tp const & X, Tp const & Y) : x(X), y(Y) { }

	template<class Up, class = typename enable_if<is_convertible<Up, Tp>::value>::type>
	constexpr Point2D(Point2D<Up> const & rhs) : x(rhs.x), y(rhs.y) { };

	template<class Up, class = typename enable_if<is_convertible<Up, Tp>::value>::type>
	constexpr Point2D(Point2D<Up> && rhs) : x(rhs.x), y(rhs.y) { };

	template<class Up, class = typename enable_if<is_convertible<Up, Tp>::value>::type>
	constexpr Point2D(Up && xx, Up && yy) : x(std::forward<Up>(xx)), y(std::forward<Up>(yy)) { };

	template<class Up, class = typename enable_if<is_convertible<Up, Tp>::value>::type>
	constexpr Point2D(Up && xx, Tp const & yy) : x(std::forward<Up>(xx)), y(yy) { };

	template<class Up, class = typename enable_if<is_convertible<Up, Tp>::value>::type>
	constexpr Point2D(Tp const & xx, Up && yy) : x(xx), y(std::forward<Up>(yy)) { };

	template<class Up, class Vp, class = typename enable_if< conditional<is_convertible<Up, Tp>::value, is_convertible<Up, Tp>, is_convertible<Vp, Tp> >::type::value>::type>
	constexpr Point2D(Up && xx, Vp && yy) : x(std::forward<Up>(xx)), y(std::forward<Vp>(yy)) { };

	Point2D & operator=(Point2D const & rhs)
	{
		x = rhs.x;
		y = rhs.y;
		return *this;
	}

	Point2D & operator=(Point2D && rhs)
	{
		x = std::forward<Tp>(rhs.x);
		y = std::forward<Tp>(rhs.y);
		return *this;
	}

	template<class Up>
	Point2D & operator=(Point2D<Up> const & rhs)
	{
		x = rhs.x;
		y = rhs.y;
		return *this;
	}

	template<class Up>
	Point2D & operator=(Point2D<Up> && rhs)
	{
		x = std::forward<Up>(rhs.x);
		y = std::forward<Up>(rhs.y);
		return *this;
	}

	constexpr bool operator==(Point2D const & rhs) const
	{
		return x==rhs.x && y==rhs.y;
	}

	constexpr bool operator==(Point2D && rhs) const
	{
		return x==rhs.x && y==rhs.y;
	}

	template<class Up>
	constexpr bool operator==(Point2D<Up> const & rhs) const
	{
		return x==rhs.x && y==rhs.y;
	}

	template<class Up>
	constexpr bool operator==(Point2D<Up> && rhs) const
	{
		return x==rhs.x && y==rhs.y;
	}

	constexpr bool operator!=(Point2D const & rhs) const
	{
		return x!=rhs.x|| y!=rhs.y;
	}

	constexpr bool operator!=(Point2D && rhs) const
	{
		return x!=rhs.x|| y!=rhs.y;
	}

	template<class Up>
	constexpr bool operator!=(Point2D<Up> const & rhs) const
	{
		return x!=rhs.x|| y!=rhs.y;
	}

	template<class Up>
	constexpr bool operator!=(Point2D<Up> && rhs) const
	{
		return x!=rhs.x|| y!=rhs.y;
	}

	Tp const & operator[](size_t const idx) const
	{
		switch(idx)
		{
			case 0: return x;
			case 1: return y;
			default: throw std::out_of_range(POINT2D_OUT_OF_RANGE_MESSAGE);
		}
	}

	Tp & operator[](size_t const idx)
	{
		switch(idx)
		{
			case 0: return x;
			case 1: return y;
			default: throw std::out_of_range(POINT2D_OUT_OF_RANGE_MESSAGE);
		}
	}

	static Point2D<Tp> const ORIGIN;
};

template<class Tp>
Point2D<Tp> const Point2D<Tp>::ORIGIN = Point2D();

}
